<?php
//   +----------------------------------------------------------------------
//   | Copyright (c) 2015-2025 http://www.hdphp.cn All rights reserved.
//   | Licensed ( http://www.hdphp.cn/licenses/ )
//   | Author: Jack <sophia2152@qq.com>
//   | 官方网址: http://www.hdphp.cn
//   | 这不是一个自由软件！您只能在不用于商业目的的前提下对程序代码进行修改和使用。
//   | 任何企业和个人不允许对程序代码以任何形式任何目的再发布。
//   +----------------------------------------------------------------------
namespace app\http\middleware\api;

use app\Request;
use think\exception\ValidateException;
use think\facade\Cache;

/**
 * Class：限流中间件
 * 创建人：Jack
 * 创建时间：2023/11/21 17:24
 * @package app\http\middleware\api
 */
class RateLimiterMiddleware
{

    /**
     * 注释：handle
     * 创建人：Jack
     * 创建时间：2023/11/21 17:24
     * @param Request  $request
     * @param \Closure $next
     * @return mixed
     */
    public function handle(Request $request, \Closure $next)
    {
        $option = $request->rule()->getOption();

        // 是否开启限流
        if (isset($option['rateLimiter']) && $option['rateLimiter']) {

            $rule = trim(strtolower($request->rule()->getRule()));
            $uid = 0;

            if ($request->hasMacro('uid') && !empty($option['isUser'])) {
                $uid = $request->uid();
            }

            $key = md5($rule . $uid);

            $this->createLimit($key, $option['limitNum'], $option['expire']);
        }

        return $next($request);
    }

    /**
     * 注释：createLimit
     * 创建人：Jack
     * 创建时间：2023/11/21 17:25
     * @param string $key
     * @param int    $initNum
     * @param int    $expire
     * @return true
     * @throws \RedisException
     */
    protected function createLimit(string $key, int $initNum, int $expire)
    {
        $nowTime = time();

        /** @var \Redis $redis */
        $redis = Cache::store('redis')->handler();

        $script = <<<LUA
    local key = KEYS[1]
    local initNum = tonumber(ARGV[1])
    local expire = tonumber(ARGV[2])
    local nowTime = tonumber(ARGV[3])

    local limitVal = redis.call('get', key)

    if limitVal then
        limitVal = cjson.decode(limitVal)
        local newNum = math.min(initNum, (limitVal.num - 1) + ((initNum / expire) * (nowTime - limitVal.time)))
        if newNum <= 0 then
            return 0
        else
            local redisVal = {num = newNum, time = nowTime}
            redis.call('set', key, cjson.encode(redisVal))
            redis.call('expire', key, expire)
            return 1
        end
    else
        local redisVal = {num = initNum, time = nowTime}
        redis.call('set', key, cjson.encode(redisVal))
        redis.call('expire', key, expire)
        return 1
    end
LUA;

        $result = $redis->eval($script, [$key, $initNum, $expire, $nowTime], 1);
        if (!$result) {
            throw new ValidateException('访问频次过多!');
        }

        return true;
    }
}
